섹션 1: 소개 및 첫 번째 뉴런 코딩

Open In Colab

NumPy를 활용한 신경망

##홈페이지로 돌아가기

이 튜토리얼은 sentdex의 Harrison Kinsley의 자료를 각색하고 이미지를 차용했습니다.

목표: 이 워크숍을 마치면 다음 개념을 이해할 수 있습니다:

  • 뉴런 (기계 학습의 맥락에서)
  • 순전파(Forward pass) / 역전파(backward pass)
  • ReLU 활성화 함수
  • Softmax 활성화 손실 및 범주형 교차 엔트로피(categorical cross entropy)
  • Adam 옵티마이저
  • 가중치(Weights)와 편향(biases) 및 각각의 업데이트
  • 학습(Training)과 에포크(epochs)

작업 내용을 저장하려면 이 노트북의 사본을 개인 Google 드라이브에 저장하세요.

목차

섹션 1: 소개 및 첫 번째 뉴런 코딩

  • 뉴런이란 무엇인가?
  • 뉴런의 입력과 출력 계산하기

섹션 2: 뉴런 레이어 코딩 * 리스트의 리스트를 사용하여 출력 계산하기 * 내적(Dot Product)의 개념 * 신경망에서 배치(batch) 사용하기

섹션 3: 은닉층 활성화 함수 * 계단 함수(Step Function) * 시그모이드 함수(Sigmoid Function) * Rectified Linear Unit (ReLU)

섹션 4: 출력층 활성화 함수 * Softmax * 오버플로우 방지

섹션 5: 손실 계산 및 구현 * 범주형 교차 엔트로피(Categorical Cross Entropy) * 출력 배치의 손실 계산 * 에러: Log(0) * 정확도 공식

섹션 6: 역전파(Backpropogation) * 편미분과 그래디언트 * 연쇄 법칙(Chain Rule)

섹션 7: 옵티마이저 * 확률적 경사 하강법(Stochastic Gradient Descent)

섹션 8: 학습률과 모멘텀 * 학습률 감쇠(Learning Rate Decay) * 모멘텀(Momentum)

섹션 9: 처음부터 완전한 신경망 구축하기

라이브러리 임포트

Code
# package for creating our dataset
!pip install nnfs
Collecting nnfs
  Downloading nnfs-0.5.1-py3-none-any.whl (9.1 kB)
Requirement already satisfied: numpy in /usr/local/lib/python3.10/dist-packages (from nnfs) (1.25.2)
Installing collected packages: nnfs
Successfully installed nnfs-0.5.1
Code
import numpy as np
import nnfs
from nnfs.datasets import spiral_data
import matplotlib.pyplot as plt
import math

nnfs.init()

왜 이것들을 신경망(neural networks)이라고 부를까요?

시각적으로 네트워크처럼 보이기 때문입니다. 파란색 원은 뉴런이고 주황색 선으로 연결되어 있습니다. 이 경우 입력층, 각각 4개의 뉴런이 있는 두 개의 은닉층, 그리고 출력층이 있습니다.

데이터는 왼쪽에서 시작하여 앞으로 전달됩니다(이 이미지에서는 입력으로 들어오는 데이터가 2개뿐입니다). 결국 우리가 원하는 것을 반환하는 출력층으로 전달됩니다. 출력의 예로는 이미지를 고양이 또는 개로 분류하는 것이 있습니다. 출력 뉴런 중 하나는 고양이 예측과 관련되고 다른 하나는 개 예측과 관련되며, 네트워크의 예측은 어떤 뉴런이 더 강하게 발화했는지에 따라 결정됩니다.

학습 중에 가중치(weights)편향(biases)을 조정하여 신경망의 예측 정확도를 최적화할 수 있습니다. 이에 대해서는 나중에 더 자세히 논의하겠습니다…

image.png

위와 같은 완전 연결(fully connected) 신경망의 뉴런을 코딩하는 것부터 시작해 보겠습니다. 완전 연결 신경망에서는 모든 뉴런이 이전의 모든 단일 뉴런과 고유하게 연결되어 있습니다.

이전 레이어에서 3개의 뉴런이 우리가 구축하고 있는 뉴런으로 입력된다고 가정해 봅시다.

아래의 단일 뉴런을 고려해보세요:

image.png

이전 레이어(3개의 뉴런이 있음)로부터의 출력을 계산해 봅시다.

Code

# this will be the input to our current neuron
inputs = np.array([1, 2, 3])

# every unique input will have a unique weight associated with it
# since we have three inputs, we have 3 weights
weights = np.array([0.2, 0.8, -0.5])

# every unique neuron has a unique bias
bias = 2

# output from our neuron is the input*weight + bias
output = inputs[0]*weights[0] + inputs[1]*weights[1] + inputs[2]*weights[2] + bias
print(output)
2.3

이제 출력층 뉴런 중 하나(4번째 레이어, 맨 위 뉴런)를 모델링해 봅시다.

image.png

무엇이 바뀔까요? 고유한 가중치가 하나 더 생기지만, 다른 편향이 있나요? 아닙니다! 우리는 여전히 하나의 관련 편향을 가진 단일 뉴런만 모델링하고 있습니다.

이전 셀과 동일한 방법론을 사용하여 위 뉴런의 출력을 계산할 수 있나요?

Code
# <b> <font color='#569098'> 섹션 2: 뉴런 레이어 코딩
Code
각각 3개의 입력을 가진 3개의 뉴런을 모델링하는 것은 어떨까요?

섹션 2: 뉴런 레이어 코딩

각각 3개의 입력을 가진 3개의 뉴런을 모델링하는 것은 어떨까요?

image.png

3개의 뉴런이 있으므로 3개의 고유한 가중치 세트가 있게 됩니다. 각 가중치 세트에는 4개의 값(입력이 4개이므로)이 있습니다. 또한 3개의 고유한 편향이 필요합니다.

Code
{{IMG_0}}
[4.8, 1.21, 2.385]

output이 다른 것이 되기를 원한다면 어떻게 해야 할까요? 뉴런의 input은 이미 이전 레이어에서 제공되거나(또는 입력 레이어에서 옴) 변경할 수 없습니다. weightsbias는 어떨까요? 고정되어 있나요?

weightsbiases는 신경망의 조절 노브와 같아서 다른 모든 뉴런 간의 통신을 변경하기 위해 조절할 수 있다는 것이 조금 더 명확해져야 합니다.

가중치에 대한 listslist를 사용하여 위 코드를 단순화해 봅시다(여전히 3개의 뉴런). 이것은 레이어에 대해 입력*가중치 + 편향을 수행하는 더 깔끔한 방법입니다.

Code
<b><font color='#e59454'> `.shape` numpy 메서드를 사용하여 아래 각각의 모양(shape)을 출력하세요</font></b>
[4.8, 1.21, 2.385]

조정/미세조정을 위해 왜 weightsbiases 둘 다 필요할까요? 이론적으로는 둘 다 필요하지 않을 수도 있습니다. 하지만 둘 다 있으면 서로 다른 유형의 수정을 제공하므로 가능한 출력을 다양화하는 데 도움이 됩니다. 이것은 그래프 y = mx + b를 개념화하는 것과 정확히 같습니다. 기울기(가중치)를 변경하면 그래프의 가파른 정도가 바뀌고, b(편향)를 변경하면 플롯의 위치가 바뀝니다.

image.png

우리는 다양한 모양과 크기의 배열을 다룰 것입니다. 아래 세 가지를 고려해보세요:

Code
<font color='#e59454'> <b> 이전 레이어의 3 뉴런에 연결된 단일 뉴런에 대한 `output`을 계산하는 코드를 작성하세요 </b>

.shape numpy 메서드를 사용하여 아래 각각의 모양(shape)을 출력하세요

Code
## **<font color='#569098'> 배치(Batches)에 대한 코멘트</font>**

배치를 사용하면 병렬로 계산할 수 있습니다. 배치가 클수록 더 많은 병렬 연산을 실행할 수 있습니다. 이것이 바로 우리가 CPU의 4-8코어 대신 계산을 실행할 수 있는 수백-수천 개의 코어가 있는 GPU에서 신경망 학습을 수행하는 경향이 있는 이유이기도 합니다. 배치는 *일반화(generalization)*에도 도움이 됩니다. 한 번에 하나씩 또는 모든 포인트를 한 번에 노출시키는 대신 한 번에 여러 데이터 포인트를 네트워크에 노출시키는 것은 신경망이 일반화하는 데 도움이 되는 것으로 널리 이해되고 있습니다. 일반적으로 우리는 32, 64, 아마도 128 배치 크기로 작업할 것입니다.
Code
# <b> <font color='#569098'> 섹션 3: 은닉층 활성화 함수

배열은 상동적(homologous)이어야 합니다: 각 차원에서 동일한 크기를 가져야 합니다.

## 내적(Dot product) 소개

(이미 확실히 이해하고 있다면 건너뛰어도 좋습니다)

inputs(벡터)에 weights(벡터의 행렬)를 어떻게 곱할까요? 내적입니다! a와 b 사이의 내적을 수행할 수 있나요?

상기해보세요:

\(a \cdot b= \sum \limits_ {i=1}^n a_i b_i\)

아래 코드 상자에 내적 코드를 작성하세요

Code
 따라서 계단 함수의 경우 뉴런의 출력은 말 그대로 0 또는 1입니다.

이전 레이어의 3개 뉴런에 연결된 단일 뉴런에 대한 output을 계산하는 코드를 작성하세요

Code
이것이 이론적으로는 괜찮지만, 더 세분화된 것을 사용하면 신경망 학습이 더 잘 이루어질 것이라는 점이 금방 분명해집니다. 예를 들어 계단 함수에서 출력이 0 경우, 입력 값이 출력 1 도달하는 데 얼마나 가까웠는지 어떻게 알 수 있을까요? 이것을 알면 모델이 스스로를 더 잘 수정하는 데 도움이 되며, *`loss`*를 계산하고 가중치와 편향을 *`optimizing`*할 때 작용합니다.

그러한 함수 중 하나가 <b><font color='#e59454'>시그모이드 함수</font></b>입니다. 출력이 0(또는 1)에 얼마나 가까운지(또는 멀리 있는지) 쉽게 알 수 있습니다. 그러나 시그모이드 함수조차도 완벽하지 않지만, *기울기 소실(vanishing gradients)*이라는 단점은 우리가 기울기(gradient)에 도달할 때까지는 그다지 이해되지 않을 것입니다.
4.8

아래 코드 상자에 뉴런의 dotlayer에 대한 출력을 계산하는 코드를 작성하세요

Code
이로써 <b><font color='#e59454'>Rectified Linear Unit (ReLU) 활성화 함수</font></b>에 도달하게 됩니다.

x가 0보다 크면 출력은 x이고, 그렇지 않으면 출력은 0입니다. ReLU를 사용하는 주된 이유 중 하나는 빠르다는 것입니다: x가 0보다 작으면 0 출력, x가 0보다 크면 x 출력.

왜 선형 활성화 함수(y=x와 같은)를 사용하지 않는지 의문을 가질 수 있지만, 비선형 데이터를 선형 함수로 모델링하는 것은 불가능합니다 :(
그래서 우리는 비선형 활성화 함수가 필요합니다.
[4.79999995 1.21000004 2.38499999]

배치(Batches)에 대한 코멘트

배치를 사용하면 병렬로 계산할 수 있습니다. 배치가 클수록 더 많은 병렬 연산을 실행할 수 있습니다. 이것이 바로 우리가 CPU의 4-8코어 대신 계산을 실행할 수 있는 수백-수천 개의 코어가 있는 GPU에서 신경망 학습을 수행하는 경향이 있는 이유이기도 합니다. 배치는 일반화(generalization)에도 도움이 됩니다. 한 번에 하나씩 또는 모든 포인트를 한 번에 노출시키는 대신 한 번에 여러 데이터 포인트를 네트워크에 노출시키는 것은 신경망이 일반화하는 데 도움이 되는 것으로 널리 이해되고 있습니다. 일반적으로 우리는 32, 64, 아마도 128의 배치 크기로 작업할 것입니다.

섹션 3: 은닉층 활성화 함수

활성화 함수는 은닉층(입력 또는 출력 레이어 이외의 레이어)에서 여러 가지 이유로 사용됩니다. 이들은 각 뉴런의 출력을 정규화하여 값이 네트워크를 통과하면서 너무 커지거나 작아지는 것을 방지합니다.

각 뉴런은 활성화 함수를 갖게 되며, 이는 inputs * weights + bias를 계산한 후에 적용됩니다.

따라서 output = Activation Function (inputs * weights + bias)

여기서는 3가지 활성화 함수에 대해 이야기하겠습니다. 1. 계단 함수(Step function) 2. 시그모이드 함수(Sigmoid function) 3. Rectified linear (ReLU) 함수.

계단 함수 의 경우 입력이 0보다 크면 출력은 1이고, 그렇지 않으면 출력은 0입니다.

image.png

따라서 계단 함수의 경우 뉴런의 출력은 말 그대로 0 또는 1입니다.

image.png

이것이 이론적으로는 괜찮지만, 더 세분화된 것을 사용하면 신경망 학습이 더 잘 이루어질 것이라는 점이 금방 분명해집니다. 예를 들어 계단 함수에서 출력이 0인 경우, 입력 값이 출력 1에 도달하는 데 얼마나 가까웠는지 어떻게 알 수 있을까요? 이것을 알면 모델이 스스로를 더 잘 수정하는 데 도움이 되며, loss를 계산하고 가중치와 편향을 optimizing할 때 작용합니다.

그러한 함수 중 하나가 시그모이드 함수입니다. 출력이 0(또는 1)에 얼마나 가까운지(또는 멀리 있는지) 쉽게 알 수 있습니다. 그러나 시그모이드 함수조차도 완벽하지 않지만, 기울기 소실(vanishing gradients)이라는 단점은 우리가 기울기(gradient)에 도달할 때까지는 그다지 이해되지 않을 것입니다.

image.png

이로써 Rectified Linear Unit (ReLU) 활성화 함수에 도달하게 됩니다.

x가 0보다 크면 출력은 x이고, 그렇지 않으면 출력은 0입니다. ReLU를 사용하는 주된 이유 중 하나는 빠르다는 것입니다: x가 0보다 작으면 0 출력, x가 0보다 크면 x 출력.

왜 선형 활성화 함수(y=x와 같은)를 사용하지 않는지 의문을 가질 수 있지만, 비선형 데이터를 선형 함수로 모델링하는 것은 불가능합니다 :( 그래서 우리는 비선형 활성화 함수가 필요합니다.

image.png

지금까지 배운 신경망 출력을 조정하기 위한 다양한 매개변수를 상기해 봅시다..

가중치와 편향을 조정하면 뉴런의 출력에 어떤 영향을 미치는지 다시 살펴보면… 가중치를 변경하면 활성화의 강도(기울기)가 변경되고, 편향으로 활성화 지점을 상쇄할 수 있습니다. 또한 가중치를 음수로 하여 뉴런을 비활성화하는 데 필요한 x 값을 확인할 수도 있습니다.

Code
우리는 결국 문제에 부딪힐 것입니다... <font color ='#e59454'><b> log(0)
Match the following:
1. What controls the offset of the output
2. What controls the strength/slope of the output?
3. What do we use to normalize the output?
A. Weights
B. Bias
C. Activation Function
Match for 1 (enter A, B, or C): B
Match for 2 (enter A, B, or C): A
Match for 3 (enter A, B, or C): C
Correct match for 1. What controls the offset of the output
Correct match for 2. What controls the strength/slope of the output?
Correct match for 3. What do we use to normalize the output?

inputs 배열의 각 값을 받아 각 값에 대한 ReLU functionoutput을 계산하는 알고리즘을 아래 코딩 셀에 작성하세요

Code
예측 값을 클리핑하면 이 무한대 문제에 빠지지 않도록 보장합니다.
Code
그리고 정확도는 어떻게 계산할까요?

섹션 4: 출력층 활성화 함수

왜 또 다른 활성화 함수가 필요할까요? 우리는 상대적인 “정확성”을 정량화하는 데 도움이 되는 함수를 원합니다. 아래 두 개의 출력 레이어에서 어느 것이 상대적으로 더 정확할까요? 배열의 첫 번째 노드가 정답 예측이라고 가정할 때, layer_outputs1이 다른 배열 값보다 상대적으로 크기 때문에 아마도 더 정확할 것입니다.

Code
{{IMG_0}}

Softmax 함수는 기계 학습에서 다중 클래스 분류 작업에 일반적으로 사용됩니다. 임의의 실수 값 점수(종종 로짓이라고 함) 벡터를 입력으로 받아 여러 클래스에 대한 확률 분포로 정규화합니다.

\(K\)개의 요소를 가진 벡터 \(z\)에 대한 Softmax 함수는 다음과 같이 정의됩니다:

\[ \text{softmax}(z)_i = \frac{e^{z_i}}{\sum_{j=1}^{K} e^{z_j}} \]

여기서: - $ (z)_i $는 결과 확률 분포의 \(i\)번째 요소입니다. - \(e\)는 자연 로그의 밑(오일러 수)입니다. - \(z_i\)는 입력 벡터 \(z\)\(i\)번째 요소입니다. - 분모는 모든 클래스에 대한 지수화된 점수의 합입니다.

Softmax 함수는 출력 확률의 합이 1이 되도록 보장하므로 모델이 클래스 확률을 출력해야 하는 분류 작업에 적합합니다. layer_outputs1layer_outputs2의 Softmax를 계산할 수 있나요? 각 레이어의 Softmax를 계산한 후, Softmax된 배열의 합을 출력하여 합이 1이 되는지 확인하세요

Code
{{IMG_0}}
Code
**편미분(partial derivative)**은 단일 입력이 함수의 출력에 얼마나 많은 영향을 미치는지 측정합니다.

모든 매개변수의 편미분을 결합하면 결과 방정식 세트를 <font color='#e59454'>**그래디언트(gradient)**</font>라고 합니다.

단일 레이어 출력 벡터에서 벗어나, 실제로는 출력 배치를 갖게 될 것입니다.

Code
아래 이미지와 유사하게 순전파는 함수의 체인입니다. 입력 데이터를 첫 번째 레이어로 전달하여 레이어의 가중치와 편향을 갖고 결과는 ReLU 활성화 함수를 통해 흐릅니다. 이 계산을 다른 레이어에서 반복하여 출력층과 Softmax 활성화까지 계속합니다.
[[8.95282664e-01 2.47083068e-02 8.00090293e-02]
 [9.99811129e-01 2.23163963e-05 1.66554348e-04]
 [5.13097164e-01 3.58333899e-01 1.28568936e-01]]

오버플로우 방지란 무엇인가요?

지수를 다룰 때 프로세서가 계산할 수 없는 정말 큰 값에 도달하여 장애물이 되기 쉽습니다.

Code
손실을 개선하기 위해 각 가중치와 편향이 손실에 어떤 영향을 미치는지 배워야 합니다. 함수 체인에 대해 이를 수행하려면 *연쇄 법칙(chain rule)*을 사용합니다. 이는 함수 체인의 미분은 이 체인에 있는 모든 함수의 미분의 곱이라는 것입니다.

매개변수(가중치 등)가 손실에 어떤 영향을 미치는지 계산하려면:

1. 가장 바깥쪽 함수의 다음 함수에 대한 미분을 찾는 것으로 시작합니다.
2. 여기에 그 앞의 함수에 대한 다음 함수의 미분을 곱합니다.
3. 매개변수(가중치 또는 편향)에 도달할 때까지 체인의 각 함수의 미분을 계속 곱합니다.

연쇄 법칙은 단일 입력이 최종 출력(이 경우 손실)에 어떤 영향을 미치는지 파악하는 데 필수적입니다.
RuntimeWarning: overflow encountered in exp
  np.exp(10000)
inf

우리는 모든 출력을 가져와 레이어의 가장 큰 값을 레이어의 모든 값에서 뺌으로써 신경망에서 이를 방지할 수 있습니다. 이제 가장 큰 값은 0이고 다른 모든 값은 0보다 작습니다.

섹션 5: 손실 계산 및 구현

모델을 어떻게 최적화할 수 있을까요? 예측된 레이블과 실제 레이블의 정확도에 대해 최적화해야 할까요? 이것은 좋은 전략이지만, 출력이 단일 숫자가 아닌 확률 분포이기 때문에 많은 유용한 정보를 버리게 될 것입니다.

범주형 교차 엔트로피(Categorical cross-entropy)는 신경망 분류 모델, 특히 다중 클래스 분류 작업에서 일반적으로 사용되는 손실 함수입니다. 이는 레이블의 실제 확률 분포와 모델이 출력한 예측 확률 분포 간의 비유사성을 측정합니다. 이 손실은 실제 확률과 예측 확률 간의 큰 오류에 불이익을 주어 모델이 올바른 클래스 레이블에 대해 더 높은 확률을 출력하도록 장려합니다.

\[ L(y, \hat{y}) = - \frac{1}{N} \sum_{i=1}^{N} \sum_{j=1}^{K} y_{ij} \log(\hat{y}_{ij}) \]

여기서: - $ L(y, ) $는 범주형 교차 엔트로피 손실입니다. - $ N $은 데이터셋의 샘플 수입니다. - $ K $는 클래스 수입니다. - $ y $는 모양이 $ (N, K) $인 실제 원-핫 인코딩 레이블 행렬입니다. - $ $는 모델이 출력한 예측 확률 분포 행렬로 모양은 $ (N, K) $입니다. - $ y_{ij} $는 샘플 \(i\)가 클래스 $ j $에 속할 실제 확률입니다. - \(\hat{y}_{ij}\)는 샘플 \(i\)가 클래스 \(j\)에 속할 예측 확률입니다.

Code
첫 번째 단계는 연쇄 법칙을 사용하여 각 매개변수와 입력에 대한 미분과 편미분을 계산하여 그래디언트를 역전파하는 것입니다. 우리가 직면한 중첩 함수는 아래와 같이 쓸 수 있습니다.
0.35667494393873245
Code
{{IMG_0}}
0.35667494393873245
Code
가중치와 편향에 대한 미분은 그것들의 영향에 대해 알려주며 가중치와 편향을 업데이트하는 데 사용됩니다. 입력에 대한 미분은 체인의 이전 함수로 전달하여 더 많은 레이어를 연결하는 데 사용됩니다.

손실 함수의 미분을 계산하고 모든 연속 레이어의 모든 활성화 함수와 뉴런의 미분과 함께 연쇄 법칙을 적용해야 합니다. 가중치와 편향에 대한 미분은 매개변수를 업데이트하는 데 사용됩니다. 레이어 입력에 대한 미분은 다른 레이어로 연결하는 데 사용됩니다(이것이 체인의 이전 레이어로 역전파하는 이유입니다).
0.35667494393873245 0.6931471805599453

세 이미지 배치에 대한 손실을 계산해 봅시다

Code
ReLU() 미분을 취하고, 합산 연산의 미분을 취하고, 둘 다 곱하는 등... 거꾸로 작업하는 것이 연쇄 법칙을 사용한 역전파입니다. 결과 출력 함수의 그래디언트는 이후 레이어의 후속 함수 그래디언트와 현재 함수의 그래디언트를 곱하여 NN을 통해 다시 전달됩니다.

이것은 입력, 가중치 및 편향에 대한 활성화된 뉴런의 편미분의 전체 세트입니다. 이 역전파 과정의 시각화를 보려면 [이 비디오](https://www.youtube.com/watch?v=_9qHQA30hys&t=2s)를 확인하세요.
[0.7 0.5 0.9]
Code
# <b> <font color='#569098'> 섹션 7: 옵티마이저
[0.35667494 0.69314718 0.10536052]
0.38506088005216804

우리는 결국 문제에 부딪힐 것입니다… log(0)

Code
# <b> <font color='#569098'> 섹션 8: 학습률과 모멘텀
[       inf 0.69314718 0.10536052]
RuntimeWarning: divide by zero encountered in log
  print(-np.log(softmax_outputs[[0, 1, 2], class_targets]))
Code
모델이 가능한 한 정확하기를 원하지만, 옵티마이저의 목표는 정확도를 직접 향상시키는 것이 아니라 손실을 최소화하는 것입니다. 손실은 개별 샘플 손실의 평균이므로 일부 손실은 상당히 감소할 수 있지만 다른 손실은 약간만 증가할 수 있으며, 이는 잘못된 예측으로 이어질 수 있습니다. 이는 전체 손실을 낮출 수 있지만 잘못 예측된 샘플의 수를 증가시켜 정확도를 낮출 수 있습니다.

학습 초기에는 더 큰 업데이트가 솔루션 공간의 다른 영역을 탐색하는 데 도움이 되지만, 학습이 진행됨에 따라 더 작고 더 정교한 업데이트를 원합니다. 이를 달성하기 위해 **<font color='#e59454'>학습률 감쇠(learning rate decay)</font>**를 구현합니다. 초기 학습률을 단계 수로 나누어 학습률이 처음에는 빠르게 떨어지고 나중에는 느려지도록 합니다. 이를 통해 모델은 최적의 솔루션에 가까워질수록 미세 조정을 할 수 있습니다.

지난 섹션에서는 모델을 최적화하는 방법으로 **<font color='#e59454'>SGD</font>**를 소개했습니다. 효과적일 수 있지만, 잠재적으로 모델이 손실 함수에 대한 전역 최소값을 찾는 데 도움이 될 수 있는 추가 논리 없이 그래디언트를 따르는 기본 방법입니다.

**<font color='#e59454'>모멘텀(Momentum)</font>**은 여러 업데이트에 걸쳐 그래디언트의 롤링 평균을 생성하고 이 평균을 현재 그래디언트와 결합하여 이 문제를 해결합니다. 이는 모델이 지역 최소값을 지나 손실을 더욱 줄이는 데 도움이 됩니다.
inf
RuntimeWarning: divide by zero encountered in log
  print(np.mean(-np.log(softmax_outputs[[0, 1, 2], class_targets])))

한 가지 옵션은 값을 아주 작은 양만큼 클리핑(clip)하는 것입니다.

Code
## <b><font color='#569098'>모델 정의하기</b>

아래는 모델을 만드는 데 사용할 일련의 클래스입니다. 모든 클래스를 이해할 필요는 없지만 주석을 훑어보고 각각의 일반적인 목적을 확인하세요.
16.11809565095832

예측 값을 클리핑하면 이 무한대 문제에 빠지지 않도록 보장합니다.

Code
## <b><font color='#569098'>모델 인스턴스화하기</b>

ReLU 활성화 함수를 가진 두 개의 Dense(완전 연결) 레이어로 구성된 피드포워드 신경망을 사용할 것입니다. 첫 번째 레이어는 200개의 입력 특성을 받아 64개의 뉴런을 출력하고, 두 번째 레이어는 64개의 뉴런을 받아 3개의 뉴런을 출력합니다.

그리고 정확도는 어떻게 계산할까요?

Code
## <b><font color='#569098'>학습</b>
[2 1 1]
0.6666666666666666

섹션 6: 역전파(Backpropogation)

가중치와 편향을 무작위로 검색하는 것은 비효율적입니다. 가중치와 편향을 조정하는 방법을 알기 위해서는 먼저 손실에 미치는 영향을 이해해야 합니다. 모든 입력, 가중치, 편향이 뉴런 출력과 손실 함수의 끝에 미치는 영향을 알기 위해서는, 뉴런과 전체 모델의 순전파 동안 수행되는 각 연산의 미분(derivative)을 계산해야 합니다.

image.png

미분은 덧셈/곱셈에만 국한되지 않습니다. max()와 같이 순전파에 사용되는 다른 함수에 대해서도 미분을 유도해야 합니다.

image.png

편미분(partial derivative)은 단일 입력이 함수의 출력에 얼마나 많은 영향을 미치는지 측정합니다.

모든 매개변수의 편미분을 결합하면 결과 방정식 세트를 그래디언트(gradient)라고 합니다.

image.png

아래 이미지와 유사하게 순전파는 함수의 체인입니다. 입력 데이터를 첫 번째 레이어로 전달하여 레이어의 가중치와 편향을 갖고 결과는 ReLU 활성화 함수를 통해 흐릅니다. 이 계산을 다른 레이어에서 반복하여 출력층과 Softmax 활성화까지 계속합니다.

image.png

손실을 개선하기 위해 각 가중치와 편향이 손실에 어떤 영향을 미치는지 배워야 합니다. 함수 체인에 대해 이를 수행하려면 연쇄 법칙(chain rule)을 사용합니다. 이는 함수 체인의 미분은 이 체인에 있는 모든 함수의 미분의 곱이라는 것입니다.

매개변수(가중치 등)가 손실에 어떤 영향을 미치는지 계산하려면:

  1. 가장 바깥쪽 함수의 다음 함수에 대한 미분을 찾는 것으로 시작합니다.
  2. 여기에 그 앞의 함수에 대한 다음 함수의 미분을 곱합니다.
  3. 매개변수(가중치 또는 편향)에 도달할 때까지 체인의 각 함수의 미분을 계속 곱합니다.

연쇄 법칙은 단일 입력이 최종 출력(이 경우 손실)에 어떤 영향을 미치는지 파악하는 데 필수적입니다.

image.png

단일 뉴런에 대해 ReLU 함수를 역전파하고 이 단일 뉴런에 대한 출력을 최소화하려는 것처럼 행동해 봅시다. 우리는 미분과 편미분이 포함된 연쇄 법칙을 활용하여 각 변수가 ReLU 활성화 출력에 미치는 영향을 계산할 것입니다.

image.png
Code
# neuron with 3 inputs
x = np.array([1.0, -2.0, 3.0]) # inputs
w = np.array([-3.0, -1.0, 2.0]) # weights
b = 1.0 # bias

## forward pass
# input*weights
xw0 = x[0] * w[0]
xw1 = x[1] * w[1]
xw2 = x[2] * w[2]

# add bias
z = xw0 + xw1 + xw2 + b

# ReLU activation function
y = max(z, 0)
print(y)
6.0

첫 번째 단계는 연쇄 법칙을 사용하여 각 매개변수와 입력에 대한 미분과 편미분을 계산하여 그래디언트를 역전파하는 것입니다. 우리가 직면한 중첩 함수는 아래와 같이 쓸 수 있습니다.

image.png

가중치와 편향에 대한 미분은 그것들의 영향에 대해 알려주며 가중치와 편향을 업데이트하는 데 사용됩니다. 입력에 대한 미분은 체인의 이전 함수로 전달하여 더 많은 레이어를 연결하는 데 사용됩니다.

손실 함수의 미분을 계산하고 모든 연속 레이어의 모든 활성화 함수와 뉴런의 미분과 함께 연쇄 법칙을 적용해야 합니다. 가중치와 편향에 대한 미분은 매개변수를 업데이트하는 데 사용됩니다. 레이어 입력에 대한 미분은 다른 레이어로 연결하는 데 사용됩니다(이것이 체인의 이전 레이어로 역전파하는 이유입니다).

역전파(Backward pass)

다음 레이어로부터의 미분:

\(\frac{d y}{d y} = 1.0\)

ReLU의 미분 및 연쇄 법칙:

\((1.0) * \frac{d}{d z}ReLU(z) = 1(z>0)=1(6>0)= 1.0 * 1.0 = 1.0\)

합계의 편미분 및 연쇄 법칙:

\(1.0 * \frac{\delta}{\delta (x_0 w_0)} (x_0w_0 + x_1w_1 + x_2w_2 + b) = (1.0) * (1.0) = 1.0\)

\(1.0 * \frac{\delta}{\delta (x_1 w_1)} (x_0w_0 + x_1w_1 + x_2w_2 + b) = (1.0) * (1.0) = 1.0\)

\(1.0 * \frac{\delta}{\delta (x_2 w_2)} (x_0w_0 + x_1w_1 + x_2w_2 + b) = (1.0) * (1.0) = 1.0\)

\(1.0 * \frac{\delta}{\delta b} (x_0w_0 + x_1w_1 + x_2w_2 + b) = (1.0) * (1.0) = 1.0\)

곱셈의 편미분 및 연쇄 법칙:

\((1.0) * \frac{\delta}{\delta x_0}(x_0w_0) = (1.0) * w_0 = -3.0\)

\((1.0) * \frac{\delta}{\delta w_0}(x_0w_0) = (1.0) * x_0 = 1.0\)

\((1.0) * \frac{\delta}{\delta x_1}(x_0w_0) = (1.0) * w_1 = -1.0\)

\((1.0) * \frac{\delta}{\delta w_1}(x_0w_0) = (1.0) * x_1 = -2.0\)

\((1.0) * \frac{\delta}{\delta x_2}(x_0w_0) = (1.0) * w_2 = 2.0\)

\((1.0) * \frac{\delta}{\delta w_2}(x_0w_0) = (1.0) * x_2 = 3.0\)

Code
## backward pass
# derivative from the next layer
dvalue = 1.0 # ∂y / ∂y = 1
print(dvalue)

# derivative of ReLU and chain rule
# (∂y / ∂y) * (∂ ReLU(z) / ∂
drelu_dz = dvalue * (1.0 if z > 0 else 0.)
print(drelu_dz)

# partial derivatives of summation, the chain rule
dsum_dxw0 = 1
dsum_dxw1 = 1
dsum_dxw2 = 1
dsum_db = 1
drelu_dxw0 = drelu_dz * dsum_dxw0
drelu_dxw1 = drelu_dz * dsum_dxw1
drelu_dxw2 = drelu_dz * dsum_dxw2
drelu_db = drelu_dz * dsum_db
print(drelu_dxw0, drelu_dxw1, drelu_dxw2, drelu_db)

# Partial derivatives of the multiplication and chain rule
dmul_dx0 = w[0]
dmul_dx1 = w[1]
dmul_dx2 = w[2]
dmul_dw0 = x[0]
dmul_dw1 = x[1]
dmul_dw2 = x[2]
drelu_dx0 = drelu_dxw0 * dmul_dx0
drelu_dw0 = drelu_dxw0 * dmul_dw0
drelu_dx1 = drelu_dxw1 * dmul_dx1
drelu_dw1 = drelu_dxw1 * dmul_dw1
drelu_dx2 = drelu_dxw2 * dmul_dx2
drelu_dw2 = drelu_dxw2 * dmul_dw2
print(drelu_dx0, drelu_dw0, drelu_dx1, drelu_dw1, drelu_dx2, drelu_dw2)
1.0
1.0
1.0 1.0 1.0 1.0
-3.0 1.0 -1.0 -2.0 2.0 3.0

ReLU() 미분을 취하고, 합산 연산의 미분을 취하고, 둘 다 곱하는 등… 거꾸로 작업하는 것이 연쇄 법칙을 사용한 역전파입니다. 결과 출력 함수의 그래디언트는 이후 레이어의 후속 함수 그래디언트와 현재 함수의 그래디언트를 곱하여 NN을 통해 다시 전달됩니다.

이것은 입력, 가중치 및 편향에 대한 활성화된 뉴런의 편미분의 전체 세트입니다. 이 역전파 과정의 시각화를 보려면 이 비디오를 확인하세요.

섹션 7: 옵티마이저

우리는 계산된 그래디언트를 사용하여 가중치와 편향을 조정하여 궁극적으로 손실 측정값을 줄이고자 합니다. 이런 방식으로 뉴런의 활성화 함수(ReLU) 출력을 성공적으로 줄일 수 있습니다. 이전 접근 방식에서는 이 조정을 달성하기 위해 각 가중치와 편향에서 그래디언트의 일부를 뺐습니다. 수백만, 수십억, 심지어 더 많은 차원이 있는 경우, 확률적 경사 하강법(SGD)은 전역 최소값을 찾는 가장 잘 알려진 방법입니다.

섹션 8: 학습률과 모멘텀

모델이 가능한 한 정확하기를 원하지만, 옵티마이저의 목표는 정확도를 직접 향상시키는 것이 아니라 손실을 최소화하는 것입니다. 손실은 개별 샘플 손실의 평균이므로 일부 손실은 상당히 감소할 수 있지만 다른 손실은 약간만 증가할 수 있으며, 이는 잘못된 예측으로 이어질 수 있습니다. 이는 전체 손실을 낮출 수 있지만 잘못 예측된 샘플의 수를 증가시켜 정확도를 낮출 수 있습니다.

학습 초기에는 더 큰 업데이트가 솔루션 공간의 다른 영역을 탐색하는 데 도움이 되지만, 학습이 진행됨에 따라 더 작고 더 정교한 업데이트를 원합니다. 이를 달성하기 위해 학습률 감쇠(learning rate decay)를 구현합니다. 초기 학습률을 단계 수로 나누어 학습률이 처음에는 빠르게 떨어지고 나중에는 느려지도록 합니다. 이를 통해 모델은 최적의 솔루션에 가까워질수록 미세 조정을 할 수 있습니다.

지난 섹션에서는 모델을 최적화하는 방법으로 SGD를 소개했습니다. 효과적일 수 있지만, 잠재적으로 모델이 손실 함수에 대한 전역 최소값을 찾는 데 도움이 될 수 있는 추가 논리 없이 그래디언트를 따르는 기본 방법입니다.

모멘텀(Momentum)은 여러 업데이트에 걸쳐 그래디언트의 롤링 평균을 생성하고 이 평균을 현재 그래디언트와 결합하여 이 문제를 해결합니다. 이는 모델이 지역 최소값을 지나 손실을 더욱 줄이는 데 도움이 됩니다.

섹션 9: 처음부터 완전한 신경망 구축하기

배운 개념을 간단한 신경망에 구현해 봅시다. 이 워크숍의 의도는 기계 학습의 모든 측면을 다루는 것이 아니라 신경망 이면의 몇 가지 핵심 개념을 소개하는 것이었습니다(확실히 이해하려면 여러 워크숍과 여러 아키텍처 및 데이터셋을 다루는 경험이 필요합니다). 첫 번째 패스가 위협적일 수 있지만, 모델을 인스턴스화하고 학습시키는 것을 관찰하려면 아래의 각 셀을 실행해 보세요. 그렇게 한 후, 코드를 다시 한 번 살펴보세요.

모델 정의하기

아래는 모델을 만드는 데 사용할 일련의 클래스입니다. 모든 클래스를 이해할 필요는 없지만 주석을 훑어보고 각각의 일반적인 목적을 확인하세요.

Code
# Dense layer
class Layer_Dense:
  """
  Dense layer of a neural network
  Facilitates:
  - Forward propogation of data throught layer
  - Backward propogation of gradients during training
  """

  # Layer initialization
  def __init__(self, n_inputs, n_neurons):
    # Initialize weights and biases
    self.weights = 0.01 * np.random.randn(n_inputs, n_neurons)
    self.biases = np.zeros((1, n_neurons))

  # Forward pass
  def forward(self, inputs):
    # Remember input values
    self.inputs = inputs
    # Calculate output values from inputs, weights and biases
    self.output = np.dot(inputs, self.weights) + self.biases

  # Backward pass
  def backward(self, dvalues):
    # Gradients on parameters
    self.dweights = np.dot(self.inputs.T, dvalues)
    self.dbiases = np.sum(dvalues, axis=0, keepdims=True)
    # Gradient on values
    self.dinputs = np.dot(dvalues, self.weights.T)


# ReLU activation
class Activation_ReLU:
  """
  Rectified linear unit activation function
  Applied to input of neural network layer
  Introduces non-linearity into the network
  """
  # Forward pass
  def forward(self, inputs):
    # Remember input values
    self.inputs = inputs
    # Calculate output values from inputs
    self.output = np.maximum(0, inputs)

  # Backward pass
  def backward(self, dvalues):
    # Since we need to modify original variable,
    # let’s make a copy of values first
    self.dinputs = dvalues.copy()
    # Zero gradient where input values were negative
    self.dinputs[self.inputs <= 0] = 0


# Softmax classifier - combined Softmax activation
# and cross-entropy loss for faster backward step
class Activation_Softmax_Loss_CategoricalCrossentropy():
  """
  Combination of softmax activation function and categorical cross entropy loss function
  Commonly used in classification tasks
  We minimize loss by adjustng model parameters to improve performance
  """

  # create activation and loss function objectives
  def __init__(self):
    self.activation = Activation_Softmax()
    self.loss = Loss_CategoricalCrossentropy()

  # forward pass
  def forward(self, inputs, y_true):
    # output layer's activation function
    self.activation.forward(inputs)
    # set the output
    self.output = self.activation.output
    # calculate and return loss value
    return self.loss.calculate(self.output, y_true)

  # backward pass
  def backward(self, dvalues, y_true):

    # number of samples
    samples = len(dvalues)

    # if labels one-hot encoded, turn into discrete values
    if len(y_true.shape) == 2:
      y_true = np.argmax(y_true, axis=1)

    # copy so we can safely modify
    self.dinputs = dvalues.copy()
    # Calculate gradient
    self.dinputs[range(samples), y_true] -= 1
    # Normalize gradient
    self.dinputs = self.dinputs / samples


# Adam optimizer
class Optimizer_Adam:

  """
  Adam optimization algorithm to optimize parameters of neural network
  Initalize with learning rate, decay, epsilon, momentum
  Pre-update params: Adjust learning rate based on decay
  Update params: Update params using momentum and cache corrections
  Post-update params: Track number of optimization steps performed
  """

  # Initialize optimizer - set settings
  def __init__(self, learning_rate=0.001, decay=0., epsilon=1e-7, beta_1=0.9, beta_2=0.999):
    self.learning_rate = learning_rate
    self.current_learning_rate = learning_rate
    self.decay = decay
    self.iterations = 0
    self.epsilon = epsilon
    self.beta_1 = beta_1
    self.beta_2 = beta_2

  # Call once before any parameter updates
  def pre_update_params(self):
    if self.decay:
      self.current_learning_rate = self.learning_rate * (1. / (1. + self.decay * self.iterations))

  # Update parameters
  def update_params(self, layer):

    # If layer does not contain cache arrays, create them filled with zeros
    if not hasattr(layer, 'weight_cache'):
      layer.weight_momentums = np.zeros_like(layer.weights)
      layer.weight_cache = np.zeros_like(layer.weights)
      layer.bias_momentums = np.zeros_like(layer.biases)
      layer.bias_cache = np.zeros_like(layer.biases)

    # Update momentum with current gradients
    layer.weight_momentums = self.beta_1 * layer.weight_momentums + (1 - self.beta_1) * layer.dweights
    layer.bias_momentums = self.beta_1 * layer.bias_momentums + (1 - self.beta_1) * layer.dbiases

    # Get corrected momentum
    # self.iteration is 0 at first pass
    # and we need to start with 1 here
    weight_momentums_corrected = layer.weight_momentums / (1 - self.beta_1 ** (self.iterations + 1))
    bias_momentums_corrected = layer.bias_momentums / (1 - self.beta_1 ** (self.iterations + 1))

    # update cache with squared current gradients
    layer.weight_cache = self.beta_2 * layer.weight_cache + (1 - self.beta_2) * layer.dweights**2
    layer.bias_cache = self.beta_2 * layer.bias_cache + (1 - self.beta_2) * layer.dbiases**2

    # get corrected cache
    weight_cache_corrected = layer.weight_cache / (1 - self.beta_2 ** (self.iterations + 1))
    bias_cache_corrected = layer.bias_cache / (1 - self.beta_2 ** (self.iterations + 1))

    # Vanilla SGD parameter update + normalization with square root cache
    layer.weights += -self.current_learning_rate * weight_momentums_corrected / (np.sqrt(weight_cache_corrected) + self.epsilon)
    layer.biases += -self.current_learning_rate * bias_momentums_corrected / (np.sqrt(bias_cache_corrected) + self.epsilon)

  # call once after any parameter updates
  def post_update_params(self):
    self.iterations += 1


# Softmax activation
class Activation_Softmax:

  """
  Softmax activation function for multi-class classification
  Compute probabilities for each class
  """

  # Forward pass
  def forward(self, inputs):
    # Remember input values
    self.inputs = inputs
    # Get unnormalized probabilities
    exp_values = np.exp(inputs - np.max(inputs, axis=1, keepdims=True))

    # Normalize them for each sample
    probabilities = exp_values / np.sum(exp_values, axis=1, keepdims=True)

    self.output = probabilities

  # Backward pass
  def backward(self, dvalues):
    # Create uninitialized array
    self.dinputs = np.empty_like(dvalues)

    # Enumerate outputs and gradients
    for index, (single_output, single_dvalues) in enumerate(zip(self.output, dvalues)):
      # Flatten output array
      single_output = single_output.reshape(-1, 1)

      # Calculate Jacobian matrix of the output
      jacobian_matrix = np.diagflat(single_output) - np.dot(single_output, single_output.T)

      # Calculate sample-wise gradient and add it to the array of sample gradients
      self.dinputs[index] = np.dot(jacobian_matrix, single_dvalues)


# Common loss class
class Loss:

  # calculates data and regularization losses, given model output and ground truth values
  def calculate(self, output, y):

    # calculate sample losses
    sample_losses = self.forward(output, y)

    # calculate mean losses
    data_loss = np.mean(sample_losses)

    # return loss
    return data_loss


# cross entropy loss
class Loss_CategoricalCrossentropy(Loss):
  """
  Computes categorical cross entropy
  Quantifies discrepency between predicted and true class probabilities
  """

  # forward pass
  def forward(self, y_pred, y_true):

    # number samples in batch
    samples = len(y_pred)

    # clip data to prevent division by 0
    # clip both sides to not drag mean towards any value
    y_pred_clipped = np.clip(y_pred, 1e-7, 1 - 1e-7)

    # probabilities for target values (only if categorical labels)
    if len(y_true.shape) == 1:
      correct_confidences = y_pred_clipped[ range(samples), y_true ]

    # mask values (only for one-hot encoded labels)
    elif len(y_true.shape) == 2:
      correct_confidences = np.sum( y_pred_clipped * y_true, axis=1 )

    # losses
    negative_log_likelihoods = -np.log(correct_confidences)
    return negative_log_likelihoods

  # backward pass
  def backward(self, dvalues, y_true):

    # number of samples
    samples = len(dvalues)
    # Number of labels in every sample
    # We’ll use the first sample to count them
    labels = len(dvalues[0])

    if len(y_true.shape) == 1:
      y_true = np.eye(labels)[y_true]

    # calculate gradient
    self.dinputs = -y_true / dvalues
    # Normalize gradient
    self.dinputs = self.dinputs / samples

데이터셋 생성하기

우리는 입력 \(X\)(이전 워크숍과 같은 두 개의 설명적 특성인 x와 y 좌표)가 주어졌을 때 점의 색상(y \(ϵ\) \(\{\)초록, 빨강, 또는 파랑\(\}\) )을 예측하도록 신경망을 학습시킬 것입니다.

Code
# Create dataset
X, y = spiral_data(samples=100, classes=3)

plt.scatter(X[:, 0], X[:, 1], c=y, s=40, cmap='brg')

Code
# an example feature set (x,y) and it's label (color)
X[5], y[5]

모델 인스턴스화하기

ReLU 활성화 함수를 가진 두 개의 Dense(완전 연결) 레이어로 구성된 피드포워드 신경망을 사용할 것입니다. 첫 번째 레이어는 200개의 입력 특성을 받아 64개의 뉴런을 출력하고, 두 번째 레이어는 64개의 뉴런을 받아 3개의 뉴런을 출력합니다.

image.png
Code
# Create Dense layer with 2 input features and 64 output values
dense1 = Layer_Dense(2, 64)

# Create ReLU activation (to be used with Dense layer):
activation1 = Activation_ReLU()

# Create second Dense layer with 64 input features (as we take output
# of previous layer here) and 3 output values (output values)
dense2 = Layer_Dense(64, 3)

# Create Softmax classifier’s combined loss and activation
loss_activation = Activation_Softmax_Loss_CategoricalCrossentropy()

# Create optimizer
optimizer = Optimizer_Adam(learning_rate=0.05, decay=5e-7)

학습

Code
# Train in loop
for epoch in range(10001):
  # Perform a forward pass of our training data through this layer
  dense1.forward(X)

  # Perform a forward pass through activation function
  # takes the output of first dense layer here
  activation1.forward(dense1.output)

  # Perform a forward pass through second Dense layer
  # takes outputs of activation function of first layer as inputs
  dense2.forward(activation1.output)

  # Perform a forward pass through the activation/loss function
  # takes the output of second dense layer here and returns loss
  loss = loss_activation.forward(dense2.output, y)

  # Calculate accuracy from output of activation2 and targets
  # calculate values along first axis
  predictions = np.argmax(loss_activation.output, axis=1)

  if len(y.shape) == 2:
    y = np.argmax(y, axis=1)

  accuracy = np.mean(predictions==y)

  if not epoch % 100:
    print(f'epoch: {epoch}, ' + f'acc: {accuracy:.3f}, ' + f'loss: {loss:.3f}, ' + f'lr: {optimizer.current_learning_rate}')

  # backward pass
  loss_activation.backward(loss_activation.output, y)
  dense2.backward(loss_activation.dinputs)
  activation1.backward(dense2.dinputs)
  dense1.backward(activation1.dinputs)

  # update weights and biases
  optimizer.pre_update_params()
  optimizer.update_params(dense1)
  optimizer.update_params(dense2)
  optimizer.post_update_params()
epoch: 0, acc: 0.360, loss: 1.099, lr: 0.05
epoch: 100, acc: 0.670, loss: 0.705, lr: 0.04999752512250644
epoch: 200, acc: 0.797, loss: 0.522, lr: 0.04999502549496326
epoch: 300, acc: 0.847, loss: 0.430, lr: 0.049992526117345455
epoch: 400, acc: 0.887, loss: 0.344, lr: 0.04999002698961558
epoch: 500, acc: 0.910, loss: 0.303, lr: 0.049987528111736124
epoch: 600, acc: 0.907, loss: 0.276, lr: 0.049985029483669646
epoch: 700, acc: 0.917, loss: 0.252, lr: 0.049982531105378675
epoch: 800, acc: 0.920, loss: 0.245, lr: 0.04998003297682575
epoch: 900, acc: 0.930, loss: 0.228, lr: 0.049977535097973466
epoch: 1000, acc: 0.940, loss: 0.217, lr: 0.049975037468784345
epoch: 1100, acc: 0.937, loss: 0.205, lr: 0.049972540089220974
epoch: 1200, acc: 0.947, loss: 0.192, lr: 0.04997004295924593
epoch: 1300, acc: 0.947, loss: 0.184, lr: 0.04996754607882181
epoch: 1400, acc: 0.943, loss: 0.183, lr: 0.049965049447911185
epoch: 1500, acc: 0.943, loss: 0.189, lr: 0.04996255306647668
epoch: 1600, acc: 0.943, loss: 0.165, lr: 0.049960056934480884
epoch: 1700, acc: 0.943, loss: 0.161, lr: 0.04995756105188642
epoch: 1800, acc: 0.943, loss: 0.158, lr: 0.049955065418655915
epoch: 1900, acc: 0.943, loss: 0.155, lr: 0.04995257003475201
epoch: 2000, acc: 0.947, loss: 0.151, lr: 0.04995007490013731
epoch: 2100, acc: 0.943, loss: 0.148, lr: 0.0499475800147745
epoch: 2200, acc: 0.953, loss: 0.145, lr: 0.0499450853786262
epoch: 2300, acc: 0.957, loss: 0.142, lr: 0.0499425909916551
epoch: 2400, acc: 0.957, loss: 0.138, lr: 0.04994009685382384
epoch: 2500, acc: 0.957, loss: 0.135, lr: 0.04993760296509512
epoch: 2600, acc: 0.960, loss: 0.132, lr: 0.049935109325431604
epoch: 2700, acc: 0.957, loss: 0.130, lr: 0.049932615934796004
epoch: 2800, acc: 0.937, loss: 0.159, lr: 0.04993012279315098
epoch: 2900, acc: 0.960, loss: 0.125, lr: 0.049927629900459285
epoch: 3000, acc: 0.960, loss: 0.123, lr: 0.049925137256683606
epoch: 3100, acc: 0.960, loss: 0.121, lr: 0.04992264486178666
epoch: 3200, acc: 0.960, loss: 0.119, lr: 0.04992015271573119
epoch: 3300, acc: 0.960, loss: 0.117, lr: 0.04991766081847992
epoch: 3400, acc: 0.960, loss: 0.115, lr: 0.049915169169995596
epoch: 3500, acc: 0.950, loss: 0.131, lr: 0.049912677770240964
epoch: 3600, acc: 0.957, loss: 0.118, lr: 0.049910186619178794
epoch: 3700, acc: 0.960, loss: 0.113, lr: 0.04990769571677183
epoch: 3800, acc: 0.960, loss: 0.111, lr: 0.04990520506298287
epoch: 3900, acc: 0.960, loss: 0.110, lr: 0.04990271465777467
epoch: 4000, acc: 0.960, loss: 0.109, lr: 0.049900224501110035
epoch: 4100, acc: 0.960, loss: 0.107, lr: 0.04989773459295174
epoch: 4200, acc: 0.960, loss: 0.106, lr: 0.04989524493326262
epoch: 4300, acc: 0.960, loss: 0.104, lr: 0.04989275552200545
epoch: 4400, acc: 0.960, loss: 0.103, lr: 0.04989026635914307
epoch: 4500, acc: 0.960, loss: 0.102, lr: 0.04988777744463829
epoch: 4600, acc: 0.960, loss: 0.101, lr: 0.049885288778453954
epoch: 4700, acc: 0.960, loss: 0.100, lr: 0.049882800360552884
epoch: 4800, acc: 0.960, loss: 0.099, lr: 0.04988031219089794
epoch: 4900, acc: 0.960, loss: 0.098, lr: 0.049877824269451976
epoch: 5000, acc: 0.960, loss: 0.098, lr: 0.04987533659617785
epoch: 5100, acc: 0.967, loss: 0.107, lr: 0.04987284917103844
epoch: 5200, acc: 0.960, loss: 0.097, lr: 0.04987036199399661
epoch: 5300, acc: 0.960, loss: 0.096, lr: 0.04986787506501525
epoch: 5400, acc: 0.960, loss: 0.095, lr: 0.04986538838405724
epoch: 5500, acc: 0.960, loss: 0.094, lr: 0.049862901951085496
epoch: 5600, acc: 0.960, loss: 0.094, lr: 0.049860415766062906
epoch: 5700, acc: 0.960, loss: 0.093, lr: 0.0498579298289524
epoch: 5800, acc: 0.960, loss: 0.092, lr: 0.04985544413971689
epoch: 5900, acc: 0.960, loss: 0.092, lr: 0.049852958698319315
epoch: 6000, acc: 0.960, loss: 0.091, lr: 0.04985047350472258
epoch: 6100, acc: 0.960, loss: 0.090, lr: 0.04984798855888967
epoch: 6200, acc: 0.960, loss: 0.089, lr: 0.049845503860783506
epoch: 6300, acc: 0.960, loss: 0.089, lr: 0.049843019410367055
epoch: 6400, acc: 0.960, loss: 0.088, lr: 0.04984053520760327
epoch: 6500, acc: 0.953, loss: 0.126, lr: 0.049838051252455155
epoch: 6600, acc: 0.960, loss: 0.090, lr: 0.049835567544885655
epoch: 6700, acc: 0.960, loss: 0.088, lr: 0.04983308408485778
epoch: 6800, acc: 0.960, loss: 0.088, lr: 0.0498306008723345
epoch: 6900, acc: 0.960, loss: 0.087, lr: 0.04982811790727884
epoch: 7000, acc: 0.960, loss: 0.086, lr: 0.04982563518965381
epoch: 7100, acc: 0.960, loss: 0.086, lr: 0.049823152719422406
epoch: 7200, acc: 0.960, loss: 0.085, lr: 0.049820670496547675
epoch: 7300, acc: 0.960, loss: 0.085, lr: 0.04981818852099264
epoch: 7400, acc: 0.960, loss: 0.084, lr: 0.049815706792720335
epoch: 7500, acc: 0.960, loss: 0.083, lr: 0.0498132253116938
epoch: 7600, acc: 0.960, loss: 0.083, lr: 0.04981074407787611
epoch: 7700, acc: 0.960, loss: 0.082, lr: 0.049808263091230306
epoch: 7800, acc: 0.960, loss: 0.082, lr: 0.04980578235171948
epoch: 7900, acc: 0.963, loss: 0.081, lr: 0.04980330185930667
epoch: 8000, acc: 0.963, loss: 0.081, lr: 0.04980082161395499
epoch: 8100, acc: 0.963, loss: 0.080, lr: 0.04979834161562752
epoch: 8200, acc: 0.963, loss: 0.084, lr: 0.04979586186428736
epoch: 8300, acc: 0.963, loss: 0.080, lr: 0.04979338235989761
epoch: 8400, acc: 0.963, loss: 0.079, lr: 0.04979090310242139
epoch: 8500, acc: 0.963, loss: 0.079, lr: 0.049788424091821805
epoch: 8600, acc: 0.963, loss: 0.078, lr: 0.049785945328062006
epoch: 8700, acc: 0.963, loss: 0.078, lr: 0.0497834668111051
epoch: 8800, acc: 0.963, loss: 0.077, lr: 0.049780988540914256
epoch: 8900, acc: 0.963, loss: 0.077, lr: 0.0497785105174526
epoch: 9000, acc: 0.963, loss: 0.076, lr: 0.04977603274068329
epoch: 9100, acc: 0.963, loss: 0.076, lr: 0.04977355521056952
epoch: 9200, acc: 0.963, loss: 0.075, lr: 0.049771077927074414
epoch: 9300, acc: 0.963, loss: 0.075, lr: 0.0497686008901612
epoch: 9400, acc: 0.707, loss: 1.982, lr: 0.04976612409979302
epoch: 9500, acc: 0.963, loss: 0.079, lr: 0.0497636475559331
epoch: 9600, acc: 0.963, loss: 0.076, lr: 0.049761171258544616
epoch: 9700, acc: 0.963, loss: 0.076, lr: 0.0497586952075908
epoch: 9800, acc: 0.963, loss: 0.075, lr: 0.04975621940303483
epoch: 9900, acc: 0.963, loss: 0.074, lr: 0.049753743844839965
epoch: 10000, acc: 0.967, loss: 0.074, lr: 0.04975126853296942

예측 시각화하기

Code
import matplotlib.pyplot as plt

# Assuming you have access to the weights and biases of your trained model
# For the weights and biases of dense1 and dense2 layers
W1, b1 = dense1.weights, dense1.biases
W2, b2 = dense2.weights, dense2.biases

# Create a meshgrid of points covering the feature space
h = 0.02
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                     np.arange(y_min, y_max, h))

# Flatten the meshgrid points and apply the first dense layer and ReLU activation
points = np.c_[xx.ravel(), yy.ravel()]
z1 = np.dot(points, W1) + b1
a1 = np.maximum(0, z1)

# Apply the second dense layer
z2 = np.dot(a1, W2) + b2

# Apply softmax activation to get probabilities
exp_scores = np.exp(z2 - np.max(z2, axis=1, keepdims=True))
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)

# Predictions
predictions = np.argmax(probs, axis=1)
Z = predictions.reshape(xx.shape)

# Plot decision boundary
plt.contourf(xx, yy, Z, cmap='brg', alpha=0.8)

# Plot data points
plt.scatter(X[:, 0], X[:, 1], c=y, s=40, cmap='brg')
plt.xlim(xx.min(), xx.max())
plt.ylim(yy.min(), yy.max())
plt.show()

Numpy에서 처음부터 완전한 신경망을 학습시켰습니다(휴!). 이것은 꼼꼼한 작업이었지만, PyTorch와 같은 딥러닝 패키지에서 내부적으로 수행되는 작업의 양을 이해하는 데 도움이 될 것입니다. 이것이 신경망 아키텍처 및 학습의 해부학적 구조의 모든 측면에 대한 광범위한 개요는 아니었지만, (희망컨대) 주요 입문 개념 중 일부를 조금 더 잘 이해하게 되었기를 바랍니다. 준비가 되면 WS4_IntroToNNs_PyTorch.ipynb에서 PyTorch로 동일한 아키텍처를 재구현한 것도 확인해 보세요.